#!/bin/sh
#
# Download an artifact from a gitlab job, extract its content and install the configured deb
# packages.
#
# Website: https://notabug.org/sumpfralle/gitlab-artifact-download
#
# Copyright 2020 Lars Kruse <devel@sumpfralle.de>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#


set -eu

CONFIG_FILE=${CONFIG_FILE:-}
DEFAULT_CONFIG_FILES=${DEFAULT_CONFIG_FILES:-.gitlab-artifact-download.json ~/.gitlab-artifact-download.json}


assemble_gitlab_url() {
    local path="$1"
    local project_path_component
    # gitlab expects an URL encoded path component
    project_path_component=$(printf '%s' "$GITLAB_PROJECT" | jq -rR '@uri')
    printf '%s/api/v4/projects/%s/%s' "${GITLAB_BASE_URL%/}" "$project_path_component" "${path#/}"
}


run_curl() {
    curl -sS --fail --header "Private-Token: $GITLAB_AUTH_TOKEN" "$@"
}


guess_artifact_url() {
    local wanted_reference="$1"
    local artifact_by_reference_url short_wanted_reference
    if printf '%s' "$wanted_reference" | grep -q '^[1-9][0-9]*$'; then
        # a numeric job ID was given
        assemble_gitlab_url  "/jobs/$wanted_reference/artifacts"
        return 0
    fi
    artifact_by_reference_url=$(assemble_gitlab_url "/jobs/artifacts/$wanted_reference/download?job=$GITLAB_JOB_NAME")
    if run_curl --head "$artifact_by_reference_url" >/dev/null 2>&1; then
        # it seems to be a branch or a tag
        printf '%s' "$artifact_by_reference_url"
        return 0
    fi
    # probably it is a commit
    short_wanted_reference=$(printf '%s' "$wanted_reference" | cut -c 1-8)
    get_jobs_list | while read -r job_id create_date expire_date ref commit username; do
        # a partial match is sufficient
        if printf '%s' "$commit" | grep -iq -- "$short_wanted_reference"; then
            assemble_gitlab_url "/jobs/$job_id/artifacts"
            break
        fi
    done
}


process_download_directory_deb_install() {
    local data_directory="$1"
    local deb_name_regex
    deb_name_regex=$(printf '%s' "$DEB_PACKAGE_NAMES" | tr '\n' '|' | sed 's/\s/|/g')
    find "$data_directory/$DEB_PACKAGE_DIRECTORY" -type f -name "*.deb" -print0 \
        | grep --null-data --extended-regex "/($deb_name_regex)_[^/]*\.deb$" \
        | xargs -r --null dpkg -i
}


process_download_directory() {
    local data_directory="$1"
    local action
    # for now only a deb package installation is supported
    for action in ${ACTIONS:-}; do
        case "$action" in
            deb_install)
                process_download_directory_deb_install "$data_directory"
                ;;
            *)
                echo >&2 "Unknown action specified: $action (expected: deb_install)"
                ;;
        esac
    done
}


download_and_process_by_url() {
    local download_url="$1"
    local destination_directory zip_filename
    if [ -n "$DOWNLOAD_DIRECTORY" ]; then
        destination_directory=$DOWNLOAD_DIRECTORY
    else
        destination_directory=$(mktemp --directory --tmpdir gitlab-download-XXXXXXX)
    fi
    # use a sub shell for the "trap"
    (
        # configure automatic removal, if no download directory was given
        [ -z "$DOWNLOAD_DIRECTORY" ] && trap 'rm -rf "$destination_directory"' EXIT
        zip_filename=$destination_directory/archive.zip
        run_curl "$download_url" >"$zip_filename"
        unzip -q "$zip_filename" -d "$destination_directory"
        rm -f "$zip_filename"
        process_download_directory "$destination_directory"
    )
}


get_jobs_json() {
    run_curl "$(assemble_gitlab_url "/jobs?scope=success&per_page=50")"
}


get_jobs_list() {
    get_jobs_json \
        | jq -r '.[] | select(.name == "'"$GITLAB_JOB_NAME"'") | [.id, .created_at, .artifacts_expire_at, .ref, .commit.id, .user.username] | @csv' \
        | tr -d '"' \
        | tr ',' '\t'
}


load_config_content() {
    local config_content="$1"
    local verify_required_values="${2:-false}"
    local force_write_empty_values="${3:-false}"
    parse_config_value() {
        local variable_name="$1"
        local override="$2"
        local json_path="$3"
        local is_required="${4:-false}"
        local result
        if [ -n "$override" ]; then
            result=$(printf '%s' "$override")
        else
            result=$(printf '%s' "$config_content" | jq -r "$json_path // "'""')
        fi
        if "$is_required" && "$verify_required_values"; then
            if [ -z "$result" ]; then
                echo >&2 "Missing required configuration key '$variable_name' / '$json_path'."
                return 1
            fi
        fi
        if [ -n "$result" ] || "$force_write_empty_values" ; then
            # Try to assign the variable in a sub-shell.  This may fail (and exit immediately with
            # an uncatchable error), if the variable is already set.
            # Thus the first definition of a setting takes precedence.
            if ( readonly "$variable_name=$result" 2>/dev/null ); then
                readonly "$variable_name=$result"
            fi
        fi 
    }
    parse_config_value "GITLAB_BASE_URL" "${GITLAB_BASE_URL:-}" '.base_url // ""' true
    parse_config_value "GITLAB_PROJECT" "${GITLAB_PROJECT:-}" '.project // ""' true
    parse_config_value "GITLAB_JOB_NAME" "${GITLAB_JOB_NAME:-}" '.job_name // ""' true
    parse_config_value "GITLAB_AUTH_TOKEN" "${GITLAB_AUTH_TOKEN:-}" '.auth_token // ""' true
    parse_config_value "DOWNLOAD_DIRECTORY" "${DOWNLOAD_DIRECTORY:-}" '.download_directory // ""'
    parse_config_value "ACTIONS" "${ACTIONS:-}" '(.actions // []) | .[]'
    parse_config_value "DEB_PACKAGE_DIRECTORY" "${DEB_PACKAGE_DIRECTORY:-}" '.deb.package_directory // ""'
    parse_config_value "DEB_PACKAGE_NAMES" "${DEB_PACKAGE_NAMES:-}" '(.deb.package_names // []) | .[]'
}


load_config() {
    local config_filenames config_content
    config_filenames=${CONFIG_FILE:-$DEFAULT_CONFIG_FILES}
    # initialize the configuration variables (necessary, if no configuration file was found)
    load_config_content '{}'
    for config_filename in $config_filenames; do
        if [ -e "$config_filename" ]; then
            if ! config_content=$(jq . <"$config_filename" 2>/dev/null); then
                echo >&2 "Failed to parse configuration file '$config_filename'.  Probably it does not contain valid JSON data. Skipping this file."
            else
                load_config_content "$config_content"
            fi
        fi
    done
    # Process all configuration settings again, but verify that all required settings are really
    # defined now.  Additionally force even empty values to be written.
    load_config_content '{}' true true
}


if [ $# -gt 0 ]; then
    ACTION="$1"
    shift
else
    ACTION="help"
fi


case "$ACTION" in
    install|download)
        load_config
        [ $# -gt 1 ] && echo >&2 "Too many arguments given for 'install'" && exit 1
        wanted_reference=${1:-main}
        download_url=$(guess_artifact_url "$wanted_reference")
        [ -z "$download_url" ] && echo >&2 "Failed to find job for given reference: $wanted_reference" && exit 2
        download_and_process_by_url "$download_url"
        ;;
    list)
        load_config
        { echo "job_id created expires reference commit committer"; get_jobs_list; } \
            | while read -r job_id create_date expire_date ref commit username; do
                printf '%-8s %-12s %-12s %-15s %-10s %s\n' "$job_id" "$(printf '%s' "$create_date" | cut -c 1-10)" "$(printf '%s' "$expire_date" | cut -c 1-10)" "$ref" "$(printf '%s' "$commit" | cut -c 1-8)" "$username"
            done
        ;;
    list-json)
        load_config
        get_jobs_json
        ;;
    help|--help)
        echo "Syntax:  $(basename "$0")  { install | list | list-json | help }  [ARGS]"
        echo "    install [REFERENCE]     - download and process an artifact (commit ID, branch, tag or job ID; REFERENCE defaults to 'main')"
        echo "    list                    - list the most recently built artifacts (including their expire date)"
        echo "    list-json               - list the most recently built artifacts (including their expire date, JSON format)"
        echo "    help"
        echo
        ;;
    *)
        "$0" help >&2
        exit 1
        ;;
esac
